/**
 * ...
 * @author Franco Ponticelli
 */

package uform.model.generator;

import haxe.macro.Context;
import haxe.macro.Expr;
import haxe.macro.Type;
import uform.model.generator.Granularity;
import uform.model.generator.FieldKind;
using Types;

class MacroExtractor extends BaseInfoExtractor
{
	public function extract(type : Type) : Hash<FieldInfo<Dynamic>>
	{
		switch(type)
		{
			case TInst(t, params):
				extractFieldsFromType(t.get(), params);
			default:
				error("unable to extract info from: " + type);
		}
		return definitions;
	}
	
	function error(msg : String, ?p : Position)
	{
		Context.error(msg, null != p ? p : Context.currentPos());
	}
	
	function extractFieldsFromType(type : ClassType, params : Array<Type>)
	{
		var fields = extractFields(type);
		
		var i = 0;
		for (field in fields)
		{
			if (skipField(field))
				continue;
			var info = {
				name         : field.name,
				fieldType    : null,
				expr         : null,
				defaultValue : null,
				granularity  : Mandatory,
				weight       : i++,
				label        : null,
				description  : null,
				min          : null,
				max          : null,
			};
			
			extractInfoFromType(field.type, info);
			for (m in field.meta.get())
			{
				if (m.name == "field")
					extractInfoFromMeta(extractMetaField(m.params), info);
			}
//			trace(info);
			definitions.set(field.name, info);
		}
		
		return definitions;
	}

	function extractInfoFromMeta(meta : Hash<Dynamic>, info : FieldInfo<Dynamic>)
	{
		switch(info.granularity)
		{
			case Range(min, max):
				var nmin = meta.exists("rangemin") ? Std.parseInt(meta.get("rangemin")) : 0 /*(null == optional ? min : (optional ? 0 : 1))*/;
				var nmax = meta.exists("rangemax") ? Std.parseInt(meta.get("rangemax")) : max;
				info.granularity = Range(nmin, nmax);
			default:
				//
		}

		if (meta.exists("expr") && meta.exists("value"))
			error("you can't have both a default value and an expression");
		
		if (meta.exists("value"))
			info.defaultValue = meta.get("value");
			
		if (meta.exists("expr"))
			info.expr = meta.get("expr");

		if (meta.exists("kind"))
		{
			var t = meta.get("kind");
			if (null == t || !Std.is(t, String) || (t = StringTools.trim(t).toLowerCase()) == '')
				error("metadata kind must be a string");
			switch(info.fieldType)
			{
				case ValueField(des):
					switch(des.type)
					{
						case "uform.model.value.StringType":
							switch(t)
							{
								case "email":
									var validateDomain = quote(meta.get("validateDomain") != false);
									des.validators.push( { type : "thx.validation.EmailValidator", args : [validateDomain] } );
									des.meta.set("input-type", '"email"');
									des.meta.set("email-validate-domain", validateDomain);
								case "file":
									des.meta.set("input-type", '"file"');
									meta.get("mimetypes").ifIs(Array, function(m) {
										if (m.length == 0)
											des.meta.set("mime-types", '[]');
										else
											des.meta.set("mime-types", '["' + m.join('", "') + '"]');
									});
								case "multiline":
									des.meta.set("input-type", '"multiline"');
								case "password":
									des.meta.set("input-type", '"password"');
								case "singleline":
									des.validators.push( { type : "thx.validation.SingleLineValidator", args : [] } );
									des.meta.set("input-type", '"singleline"');
								case "url":
									des.validators.push( { type : "thx.validation.UrlValidator", args : [] } );
									des.meta.set("input-type", '"url"');
								default:
									error("invalid String kind '" + t + "'");
							}
						case "uform.model.value.DateType":
							switch(t)
							{
								case "year":
									des.args[0] = "uform.util.DateGranularity.Year";
								case "month":
									des.args[0] = "uform.util.DateGranularity.Month";
								case "day", "date":
									des.args[0] = "uform.util.DateGranularity.Day";
								case "hour":
									des.args[0] = "uform.util.DateGranularity.Hour";
								case "minute":
									des.args[0] = "uform.util.DateGranularity.Minute";
								case "second":
									des.args[0] = "uform.util.DateGranularity.Second";
								default:
									error("invalid Date kind '" + t + "'");
							}
						default:
							//
					}
				default:
					//
			}
		}
		
		if (meta.exists("options"))
		{
			var options = meta.get("options").as(Array);
			if (null == options)
				error("metadata options must be an array");
			switch(info.fieldType)
			{
				case ValueField(des):
					des.validators.push( { type : "thx.validation.OptionValidator", args : [quote(options)] } );
				default:
					//
			}
		}

		if (meta.exists("weight"))
			info.weight = Std.parseInt(meta.get("weight"));
		if (meta.exists("label"))
			info.label = Std.string(meta.get("label"));
		if (meta.exists("description"))
			info.description = Std.string(meta.get("description"));
		if (meta.exists("min"))
			info.min = Std.parseInt(meta.get("min"));
		if (meta.exists("max"))
			info.max = Std.parseInt(meta.get("max"));
	}
	
	function quote(v : Dynamic) : String
	{
		if (Std.is(v, Int) || Std.is(v, Float))
			return "" + v;
		else if (Std.is(v, Bool))
			return v ? "true" : "false";
		else if (Std.is(v, String))
			return '"' + StringTools.replace(v, '"', '\\"') + '"';
		else if (Std.is(v, Array))
		{
			var arr = [];
			var values : Array<Dynamic> = cast v;
			for (value in values)
				arr.push(quote(value));
			return "[" + arr.join(", ") + "]";
		} else {
			error("quoting is not supported for: " + v);
			return null;
		}
	}
	
	function extractMetaField(a : Array<Expr>) : Hash<Dynamic>
	{
		var field = new Hash();
		for (expr in a)
		{
			switch(expr.expr)
			{
				case EObjectDecl(fields):
					for (f in fields)
					{
						var key = f.field;
						var value = extractValue(f.expr);
						field.set(key, value);
					}
				default:
					error("invalid metadata value", expr.pos);
			}
		}
		return field;
	}
	
	function extractValue(e : Expr) : Dynamic
	{
		switch(e.expr)
		{
			case EConst(c):
				switch(c)
				{
					case CInt(v)   : return Std.parseInt(v);
					case CFloat(f) : return Std.parseFloat(f);
					case CString(s): return s;
					case CIdent(s) :
						switch(s)
						{
							case "true" : return true;
							case "false": return false;
							default:
								error("invalid value: " + s, e.pos);
						}
					default:
						error("invalid value: " + c, e.pos);
				}
			case EObjectDecl(fields):
				var ob : Dynamic = { };
				for (f in fields)
					Reflect.setField(ob, f.field, extractValue(f.expr));
				return ob;
			case EArrayDecl(values):
				var arr = [];
				for (v in values)
					arr.push(extractValue(v));
				return arr;
			default:
				error("invalid value: " + e, e.pos);
		}
		return null;
	}
	
	function extractInfoFromType(t : Type, info : FieldInfo<Dynamic>)
	{
		switch(t)
		{
			case TType(def, params):
				if ("Null" == def.toString())
				{
					if (null == info.granularity)
						info.granularity = Optional;
					else
					{
						switch(info.granularity)
						{
							case Range(min, max):
								if (1 == min)
									info.granularity = Range(0, max);
							default:
								info.granularity = Optional;
						}
					}
					extractInfoFromType(params[0], info);
				}
			case TInst(type, params):
				switch(type.toString())
				{
					case "Int":
						var def = definition("uform.model.value.FloatType");
						def.validators.push({ type : "thx.validation.IncrementValidator", args : ["1"] });
						info.fieldType = ValueField(def);
					case "Float":
						info.fieldType = ValueField(definition("uform.model.value.FloatType"));
					case "Array", "List":
						info.granularity = Range(1, null);
						extractInfoFromType(params[0], info);
					case "Date":
						info.fieldType = ValueField(definition("uform.model.value.DateType"));
					case "String":
						info.fieldType = ValueField(definition("uform.model.value.StringType"));
					default: // custom class
						info.fieldType = DefinitionsField(new MacroExtractor().extractFieldsFromType(type.get(), params));
				}
			case TEnum(type, params):
				if ("Bool" == type.toString())
				{
					info.fieldType = ValueField(definition("uform.model.value.BoolType"));
				}
			default:
				//
		}
	}
	
	static function definition(type : String)
	{
		var meta = new Hash();
		if (type == "uform.model.value.StringType")
			meta.set("input-type", '"singleline"');
		return {
			type : type,
			args : [],
			validators : [],
			meta : meta
		};
	}
	
	function skipField(field : ClassField)
	{
		if (!field.isPublic)
			return true;
		return !canBuildSchema(field.type);
	}
	
	function extractFields(t : ClassType)
	{
		var fields = [];
		if (t.isInterface)
			extractInterfaceFields(t, fields);
		else
			extractClassFields(t, fields);
		return fields;
	}
	
	function extractInterfaceFields(t : ClassType, fields : Array<ClassField>)
	{
		for (i in t.interfaces)
		{
			var inter = i.t.get();
			extractInterfaceFields(inter, fields);
			var nfields = inter.fields.get();
			for (field in nfields)
				fields.push(field);
		}
	}
	
	function extractClassFields(t : ClassType, fields : Array<ClassField>)
	{
		if (null == t)
			return;
		if(null != t.superClass)
			extractClassFields(t.superClass.t.get(), fields);
		var nfields = t.fields.get();
		for (field in nfields)
			fields.push(field);
	}
	
	function canBuildSchema(type : Type)
	{
		switch(type)
		{
			case TMono:
				return false;
			case TEnum(t, params):
				return "Bool" == t.toString();
			case TInst(t, params):
				return true;
			case TType(t, params):
				return "Null" == t.toString();
			case TFun(args, ret):
				return false;
			case TAnonymous(a):
				return false;
			case TDynamic(t):
				return false;
		}
	}
}